/*******************************************************************************
 * Copyright (c) 2008-2010 Cedric Vidal
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Cedric Vidal - original idea, initial contribution and API
 *******************************************************************************/

package org.trans4j.impl;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.LazyLoader;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import org.trans4j.CannotResolveUnreturnedObjectException;
import org.trans4j.CreateRule;

/**
 * @author <a href="mailto:c.vidal@proxiad.com">Cedric Vidal, ProxiAD Group</a>
 * 
 */
public class CachedMapper extends AbstractDelegateMethodInterceptor {

	private CacheSupport<Integer, Object> cacheSupport = new CacheSupport<Integer, Object>();
	private Hasher hasher = new Hasher();

	public CachedMapper() {
		super();
	}

	public final Object intercept(Object obj, Method method, Object[] args,
			MethodProxy proxy) throws Throwable {
		boolean topLevel = !cacheSupport.isCacheActive();
		Object object = null;
		try {
			object = doInvoke(obj, method, args, proxy);
		} finally {
			if (topLevel) {
				cacheSupport.releaseCache();
			}
		}
		return object;
	}

	private Object doInvoke(Object obj, final Method method, Object[] args,
			MethodProxy proxy) throws Throwable {
		Object resultOrLazyProxy;
		if (!isCreateRule(method)) {
			final Integer hash = hasher.hash(method, args);
			resultOrLazyProxy = cacheSupport.cached(hash);
			if (resultOrLazyProxy == null) {
				NotResolvedHolder holder = new NotResolvedHolder();
				holder.method = method;
				cacheSupport.cache(hash, holder);
				holder.actualResult = proxy.invokeSuper(obj, args);

				if (holder.lazyProxy != null) {
					// this means the proxy has been referenced so we need to
					// return it to maintain identity consistency
					resultOrLazyProxy = holder.lazyProxy;
				} else {
					resultOrLazyProxy = holder.actualResult;
				}

			} else {
				if (resultOrLazyProxy instanceof NotResolvedHolder) {
					final NotResolvedHolder holder = (NotResolvedHolder) resultOrLazyProxy;
					if (holder.lazyProxy == null) {
						Class<?> expectedClass = holder.method.getReturnType();
						Enhancer e = new Enhancer();
						e.setSuperclass(expectedClass);
						e.setCallbackFilter(new CallbackFilter() {
							public int accept(Method method) {
								if (isEqualsMethod(method)) {
									return 1;
								} else {
									return 0;
								}
							}
						});
						e.setCallbacks(new Callback[] {
								new HolderLazyLoader(holder),
								EqualsMethodInterceptor.INSTANCE });
						holder.lazyProxy = e.create();
					}
					resultOrLazyProxy = holder.lazyProxy;
				}
			}
		} else {
			resultOrLazyProxy = invokeDelegateOrSuper(obj, method, args, proxy);
		}
		return resultOrLazyProxy;
	}

	private static class EqualsMethodInterceptor implements MethodInterceptor {
		public final static EqualsMethodInterceptor INSTANCE = new EqualsMethodInterceptor();

		public Object intercept(Object obj, Method method, Object[] args,
				MethodProxy proxy) throws Throwable {
			Object unboxedThis = unboxProxy(obj);
			Object unboxedArg = unboxProxy(args[0]);
			boolean equals = unboxedThis.equals(unboxedArg);
			return equals;
		}

	}

	public static Object unboxProxy(Object obj) {
		Object unboxed = obj;
		if (obj instanceof Factory) {
			Factory factory = (Factory) obj;
			HolderLazyLoader holderCallback = (HolderLazyLoader) factory
					.getCallback(0);
			unboxed = holderCallback.getHolder().actualResult;
		}
		return unboxed;
	}

	private static class HolderLazyLoader implements LazyLoader {

		private final NotResolvedHolder holder;

		public NotResolvedHolder getHolder() {
			return holder;
		}

		private HolderLazyLoader(NotResolvedHolder holder) {
			this.holder = holder;
		}

		public Object loadObject() throws Exception {
//			System.out.println("Resolving proxy " + this + " for holder "
//					+ holder.method + " with actual result "
//					+ holder.actualResult);
			if (holder.actualResult == null) {
				throw new CannotResolveUnreturnedObjectException(holder.method);
			}
			return holder.actualResult;
		}
	}

	private static class NotResolvedHolder {
		protected Object actualResult;
		public Object lazyProxy;
		Method method;
	}

	protected boolean isCreateRule(Method method) {
		CreateRule createRule = method.getAnnotation(CreateRule.class);
		boolean isCreateRule = createRule != null;
		return isCreateRule;
	}

	private boolean isEqualsMethod(Method method) {
		return "equals".equals(method.getName())
				&& (method.getParameterTypes().length == 1)
				&& method.getParameterTypes()[0] == Object.class;
	}

}
